#!/usr/bin/env python
import re

#regexes for parsing snip sources
SNIP_BASENAME_REGEX = re.compile(r'\s*/\*\s*snipon:\s*(?P<basename>\S+)\s*\*/')
SNIP_START_REGEX = re.compile(r'\s*/\*\s*snip:\s*(?P<snip>\S+)\s*\*/')
SNIP_STOP_KEY_REGEX = re.compile(r'\s*/\*\s*pins:\s*(?P<snip>\S+)\s*\*/')
SNIP_STOP_REGEX = re.compile(r'\s*/\*\s*pins\s*\*/')

#regexes for expanding files with snip includes
SNIP_INCLUDE_REGEX = re.compile(r'\s*includesnip:\s*(?P<snip>\S+)\s*')

#regexes for post processors
LEADING_WHITESPACE_STRIP = re.compile(r'(\s+).*')
MULTILINE_COMMENT_STRIP = re.compile(r'\n\s*/\*.*?\*/\s*?\n', re.DOTALL)
SINGLELINE_COMMENT_STRIP = re.compile(r'\s*//.*')

def false_or_first_group(regex, line):
  '''Return false if the compiled regex does not match the line.
  Otherwise return the first match.
  '''
  matches = regex.match(line)
  if(matches == None):
    return False
  else:
    return matches.groups()[0]

def is_snip_file_header(line):
  '''Return false if line is not the start of a snip file.
  Return snip file basename if line is the start of a snip file.

  >>> is_snip_file_header("/* snipon: test.java */")
  'test.java'
  >>> is_snip_file_header("    /*     snipon: test.java   */    ")
  'test.java'
  >>> is_snip_file_header("/*snipon: my_concise_snip*/")
  'my_concise_snip'
  >>> is_snip_file_header("/* snapon: test.java */")
  False
  >>> is_snip_file_header("/* snapon: test java */")
  False
  >>> is_snip_file_header('/* snipon: org.jhpc.test */')
  'org.jhpc.test'
  '''
  return false_or_first_group(SNIP_BASENAME_REGEX, line)

def iterate_until_snipon(iterable):
  '''Iterate through the iterable (usually a file handle)
  until the snip header is found or the StopIterate exception
  is thrown.  If the header is found, the iterator and the
  basename of the snip file are returned.'''
  i = iterable.__iter__()

  while(True):
    line = i.next()
    is_snip = is_snip_file_header(line)

    if(is_snip):
      return (i, is_snip)

def is_snip_start(line):
  '''Return False if line is not a snip start.
  Return dict of snip key if line is a snip.

  >>> is_snip_start("/* snip: listing1 */")
  'listing1'
  >>> is_snip_start("  /*     snip: listing1   */    ")
  'listing1'
  >>> is_snip_start("/*snip: my_concise_snip*/")
  'my_concise_snip'
  >>> is_snip_start("/* snap: test.java */")
  False
  >>> is_snip_start("/* snap: test java */")
  False
  '''
  return false_or_first_group(SNIP_START_REGEX, line)

def is_snip_stop(line):
  '''Return False if line is not a snip end.
  Return True if line is a snip stop without a key.
  Return the key if the line is a snip stop with the key.

  >>> is_snip_stop("/* pins: listing1 */")
  'listing1'
  >>> is_snip_stop("     /*    pins: listing1    */")
  'listing1'
  >>> is_snip_stop("/* pins */")
  True
  >>> is_snip_stop("     /*    pins     */  ")
  True
  >>> is_snip_stop("     /*    snip     */  ")
  False
  '''
  if(SNIP_STOP_REGEX.match(line)):
    return True
  else:
    return false_or_first_group(SNIP_STOP_KEY_REGEX, line)

def dict_map(func, mydict):
  """Map a function across the values of a dictionary.
  Return a dictionary."""
  new_dict = {}
  for (id, snip) in mydict.iteritems():
    new_dict[id] = func(snip)
  return new_dict

def get_raw_snips(iterable):
  '''Return a dictionary ({'snip_name':'snip_contents'} of 
  all the snips in an iterable.'''

  try:
    snip_file, basename = iterate_until_snipon(iterable)
  except StopIteration:
    return dict()


  finished_snips = dict()
  pending_snips = dict()
  snips_stack = []

  for line in snip_file:

    is_start = is_snip_start(line)
    is_stop = is_snip_stop(line)

    if(is_start):
      snips_stack.append(is_start)

      pending_snips["%s:%s" % (basename, is_start)] = []

      #print "Found start: " + is_start
    elif(is_stop==True):
      #print "Found empty end."
      pass
    elif(is_stop):
      name = "%s:%s" % (basename, is_stop)

      finished_snips[name] = pending_snips[name]
      del(pending_snips[name])

      #print "Ending: " + name

    else:
      for value in pending_snips.values():
        value.append(line)

  if(pending_snips != {}):
    print "Unmatched snips."

  return dict_map(lambda x: "".join(x), finished_snips)


def post_process(funcs, snips):
  for func in funcs:
    snips = dict_map(func, snips)
  return snips
      

#post processors:
def strip_leading_whitespace(snip):
  matches = false_or_first_group(LEADING_WHITESPACE_STRIP, snip)
  if(matches):
    new_lines = []
    for line in snip.split('\n'):
      if line.startswith(matches):
        new_lines.append(line[len(matches):])
      else:
        new_lines.append(line)
    return '\n'.join(new_lines)
  else:
    return snip
    

def strip_multiline_comments(snip):
  return MULTILINE_COMMENT_STRIP.sub('\n', snip)


def strip_singleline_comments(snip):
  return SINGLELINE_COMMENT_STRIP.sub('', snip)


def get_snips(iterable, post_process_funcs=[strip_multiline_comments, 
                                            strip_singleline_comments, 
                                            strip_leading_whitespace]):

  snips = get_raw_snips(iterable)
  return post_process(post_process_funcs, snips)

if __name__ == '__main__':
   #import doctest
   #doctest.testmod()  

   testfile = open('test.java', 'r')
   snips = get_snips(testfile)
   for key, value in snips.iteritems():
     print "%s:" % key
     print "*" * 40
     print value
   #expand_file_with_snips('test_compile.tex', 'test_compile_expanded.tex', snips)






         
          
